mirror of
https://github.com/Luzifer/cloudkeys-go.git
synced 2024-11-09 22:50:05 +00:00
Reimplement Go server as API-Server
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
ed73116f3a
commit
b4793cbab5
12 changed files with 597 additions and 384 deletions
107
ajax.go
107
ajax.go
|
@ -1,107 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/flosch/pongo2"
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ajaxResponse struct {
|
|
||||||
Error bool `json:"error"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Data string `json:"data"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a ajaxResponse) Bytes() []byte {
|
|
||||||
out, _ := json.Marshal(a)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func ajaxGetHandler(c context.Context, res http.ResponseWriter, r *http.Request, session *sessions.Session, ctx *pongo2.Context) (*string, error) {
|
|
||||||
res.Header().Set("Content-Type", "application/json")
|
|
||||||
user, _ := checkLogin(c, r, session)
|
|
||||||
|
|
||||||
if user == nil || !storage.IsPresent(c, user.UserFile) {
|
|
||||||
res.Write(ajaxResponse{Error: true}.Bytes())
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
userFileRaw, err := storage.Read(c, user.UserFile)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("Could not read user file from storage")
|
|
||||||
res.Write(ajaxResponse{Error: true}.Bytes())
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
userFile, _ := readDataObject(userFileRaw)
|
|
||||||
|
|
||||||
res.Write(ajaxResponse{Version: userFile.MetaData.Version, Data: userFile.Data}.Bytes())
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ajaxPostHandler(c context.Context, res http.ResponseWriter, r *http.Request, session *sessions.Session, ctx *pongo2.Context) (*string, error) {
|
|
||||||
res.Header().Set("Content-Type", "application/json")
|
|
||||||
user, _ := checkLogin(c, r, session)
|
|
||||||
|
|
||||||
if user == nil {
|
|
||||||
res.Write(ajaxResponse{Error: true, Type: "login"}.Bytes())
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !storage.IsPresent(c, user.UserFile) {
|
|
||||||
res.Write(ajaxResponse{Error: true, Type: "register"}.Bytes())
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
userFileRaw, err := storage.Read(c, user.UserFile)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("Could not read user file from storage")
|
|
||||||
res.Write(ajaxResponse{Error: true, Type: "storage_error"}.Bytes())
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
userFile, _ := readDataObject(userFileRaw)
|
|
||||||
|
|
||||||
var (
|
|
||||||
version = r.FormValue("version")
|
|
||||||
checksum = r.FormValue("checksum")
|
|
||||||
data = r.FormValue("data")
|
|
||||||
)
|
|
||||||
|
|
||||||
if userFile.MetaData.Version != version {
|
|
||||||
res.Write(ajaxResponse{Error: true, Type: "version"}.Bytes())
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if checksum != fmt.Sprintf("%x", sha1.Sum([]byte(data))) {
|
|
||||||
res.Write(ajaxResponse{Error: true, Type: "checksum"}.Bytes())
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := storage.Backup(c, user.UserFile); err != nil {
|
|
||||||
log.WithError(err).Error("Could not create backup of user file")
|
|
||||||
res.Write(ajaxResponse{Error: true, Type: "storage_error"}.Bytes())
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
userFile.MetaData.Version = checksum
|
|
||||||
userFile.Data = data
|
|
||||||
|
|
||||||
d, _ := userFile.GetData()
|
|
||||||
|
|
||||||
if err := storage.Write(c, user.UserFile, d); err != nil {
|
|
||||||
log.WithError(err).Error("Could not write user file to storage")
|
|
||||||
res.Write(ajaxResponse{Error: true, Type: "storage_error"}.Bytes())
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Write(ajaxResponse{Version: userFile.MetaData.Version, Data: userFile.Data}.Bytes())
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
115
api.go
Normal file
115
api.go
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
userStateLoggedin userState = "logged-in"
|
||||||
|
userStateRequireMFA = "require-mfa"
|
||||||
|
)
|
||||||
|
|
||||||
|
type apiError struct {
|
||||||
|
cause error
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a apiError) Error() string { return a.msg + ": " + a.cause.Error() }
|
||||||
|
func (a apiError) Cause() error { return a.cause }
|
||||||
|
func (a apiError) UserMessage() string { return a.msg }
|
||||||
|
func wrapAPIError(cause error, msg string) error {
|
||||||
|
if cause == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return apiError{cause, msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiHelper(hdl apiHandler) http.HandlerFunc {
|
||||||
|
return func(res http.ResponseWriter, r *http.Request) {
|
||||||
|
cookieSession, err := cookieStore.Get(r, "cloudkeys-go")
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Debug("Session could not be decoded, created new one")
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := newSessionData()
|
||||||
|
if !cookieSession.IsNew {
|
||||||
|
if err := json.Unmarshal(cookieSession.Values["sessionData"].([]byte), sess); err != nil {
|
||||||
|
log.WithError(err).Debug("Session cookie contained garbled sessionData")
|
||||||
|
// Session data is garbled, create a new session in case
|
||||||
|
// something was decoded into the session object
|
||||||
|
sess = newSessionData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a pure JSON API
|
||||||
|
res.Header().Set("Content-Type", "application/json")
|
||||||
|
res.Header().Set("Cache-Control", "no-cache")
|
||||||
|
res.Header().Set("X-API-Version", version)
|
||||||
|
res.Header().Set("Access-Control-Allow-Origin", "*") // FIXME (kahlers): Remove after development
|
||||||
|
|
||||||
|
// Assign an UUID to find potiential errors in the logs
|
||||||
|
reqId := uuid.Must(uuid.NewV4()).String()
|
||||||
|
|
||||||
|
var (
|
||||||
|
resp interface{}
|
||||||
|
status int
|
||||||
|
data []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
// Define a common error handler
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"reqId": reqId,
|
||||||
|
"method": r.Method,
|
||||||
|
"path": r.URL.Path,
|
||||||
|
}).WithError(err).Error("API handler errored")
|
||||||
|
|
||||||
|
// Respond with common error format
|
||||||
|
res.WriteHeader(status)
|
||||||
|
json.NewEncoder(res).Encode(map[string]interface{}{
|
||||||
|
"error": err.(apiError).UserMessage(),
|
||||||
|
"reqId": reqId,
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Do the real work
|
||||||
|
resp, status, err = hdl(getContext(r), res, r, sess)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there was no error try to marshal the output and wrap error
|
||||||
|
// when this fails in order to have a propoer user message
|
||||||
|
data, err = json.Marshal(resp)
|
||||||
|
err = wrapAPIError(err, "Could not marshal API response")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the session data back to the cookie
|
||||||
|
sdata, err := json.Marshal(sess)
|
||||||
|
err = wrapAPIError(err, "Failed to encode session")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cookieSession.Values["sessionData"] = sdata
|
||||||
|
err = wrapAPIError(cookieSession.Save(r, res), "Failed to save session")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no error ocurred, send the response
|
||||||
|
res.WriteHeader(status)
|
||||||
|
res.Write(data)
|
||||||
|
}
|
||||||
|
}
|
343
api_funcs.go
Normal file
343
api_funcs.go
Normal file
|
@ -0,0 +1,343 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
|
|
||||||
|
openssl "github.com/Luzifer/go-openssl"
|
||||||
|
)
|
||||||
|
|
||||||
|
type apiHandler func(context.Context, http.ResponseWriter, *http.Request, *sessionData) (interface{}, int, error)
|
||||||
|
|
||||||
|
// apiChangeLoginPassword accepts three passwords: old, new and
|
||||||
|
// new-repeat. It checks the old password, compares the new ones
|
||||||
|
// and if they match it sets a new user password. In case the user
|
||||||
|
// has a MFA secret it is re-encrypted with the new password.
|
||||||
|
func apiChangeLoginPassword(ctx context.Context, res http.ResponseWriter, r *http.Request, sess *sessionData) (interface{}, int, error) {
|
||||||
|
var (
|
||||||
|
input = struct {
|
||||||
|
OldPassword string `json:"old_password"`
|
||||||
|
NewPassword string `json:"new_password"`
|
||||||
|
RepeatPassword string `json:"repeat_password"`
|
||||||
|
}{}
|
||||||
|
output = map[string]interface{}{"success": true}
|
||||||
|
username = mux.Vars(r)["user"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(input); err != nil {
|
||||||
|
return nil, http.StatusBadRequest, wrapAPIError(err, "Unable to decode login data")
|
||||||
|
}
|
||||||
|
|
||||||
|
if state, ok := sess.Users[username]; !ok || state != userStateLoggedin {
|
||||||
|
return nil, http.StatusUnauthorized, wrapAPIError(errors.New("Access to user not logged in"), "Authorization error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check new passwords matches
|
||||||
|
if input.NewPassword != input.RepeatPassword {
|
||||||
|
return nil, http.StatusBadRequest, wrapAPIError(errors.New("Password mismatch"), "New passwords do not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve data file
|
||||||
|
userFile := createUserFilename(username)
|
||||||
|
user, err := dataObjectFromStorage(ctx, storage, userFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, http.StatusInternalServerError, wrapAPIError(err, "Unable to retrieve data file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check bcrypt password and deprecated version of password
|
||||||
|
deprecatedPassword := fmt.Sprintf("%x", sha1.Sum([]byte(cfg.PasswordSalt+input.OldPassword))) // Here for backwards compatibility
|
||||||
|
if bcrypt.CompareHashAndPassword([]byte(user.MetaData.Password), []byte(input.OldPassword)) != nil &&
|
||||||
|
user.MetaData.Password != deprecatedPassword {
|
||||||
|
return nil, http.StatusUnauthorized, wrapAPIError(errors.New("Password mismatch"), "Authorization error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user password
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.NewPassword), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, http.StatusInternalServerError, wrapAPIError(err, "Unable to generate bcrypt hash")
|
||||||
|
}
|
||||||
|
user.MetaData.Password = string(hashedPassword)
|
||||||
|
|
||||||
|
// In case a MFA token is present re-encrypt it with the new password
|
||||||
|
if user.MetaData.MFASecret != "" {
|
||||||
|
secret, err := openssl.New().DecryptBytes(input.OldPassword, []byte(user.MetaData.MFASecret), openssl.DigestSHA256Sum)
|
||||||
|
if err != nil {
|
||||||
|
return nil, http.StatusInternalServerError, wrapAPIError(err, "Could not decrypt MFA secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err = openssl.New().EncryptBytes(input.NewPassword, secret, openssl.DigestSHA256Sum)
|
||||||
|
user.MetaData.MFASecret = string(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save back the user file
|
||||||
|
if err := user.writeToStorage(ctx, storage, userFile); err != nil {
|
||||||
|
return nil, http.StatusInternalServerError, wrapAPIError(err, "Could not write data file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return output, http.StatusOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiGetUserData retrieves the user file and returns the encrypted
|
||||||
|
// data together with the current checksum of the content
|
||||||
|
func apiGetUserData(ctx context.Context, res http.ResponseWriter, r *http.Request, sess *sessionData) (interface{}, int, error) {
|
||||||
|
var username = mux.Vars(r)["user"]
|
||||||
|
|
||||||
|
if state, ok := sess.Users[username]; !ok || state != userStateLoggedin {
|
||||||
|
return nil, http.StatusUnauthorized, wrapAPIError(errors.New("Access to user not logged in"), "Authorization error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve data file
|
||||||
|
userFile := createUserFilename(username)
|
||||||
|
user, err := dataObjectFromStorage(ctx, storage, userFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, http.StatusInternalServerError, wrapAPIError(err, "Unable to retrieve data file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"checksum": user.MetaData.Version,
|
||||||
|
"data": user.Data,
|
||||||
|
}, http.StatusOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiGetUserSettings(ctx context.Context, res http.ResponseWriter, r *http.Request, sess *sessionData) (interface{}, int, error) {
|
||||||
|
// FIXME (kahlers): Implement this
|
||||||
|
return nil, http.StatusInternalServerError, wrapAPIError(errors.New("Not implemented yet"), "Not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiListUsers returns a dictionary of usernames with their current login state
|
||||||
|
func apiListUsers(ctx context.Context, res http.ResponseWriter, r *http.Request, sess *sessionData) (interface{}, int, error) {
|
||||||
|
return sess.Users, http.StatusOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiLogin retrieves an username and a password, loads the user file
|
||||||
|
// from storage and compares the passwords. If the user has no MFA they
|
||||||
|
// are logged in, otherwise they require MFA auth. After login the user
|
||||||
|
// file is automatically migrated to the latest version.
|
||||||
|
func apiLogin(ctx context.Context, res http.ResponseWriter, r *http.Request, sess *sessionData) (interface{}, int, error) {
|
||||||
|
var (
|
||||||
|
input = &struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}{}
|
||||||
|
output = map[string]interface{}{"success": true}
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(input); err != nil {
|
||||||
|
return nil, http.StatusBadRequest, wrapAPIError(err, "Unable to decode login data")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := sess.Users[input.Username]; ok {
|
||||||
|
// Already logged in
|
||||||
|
return output, http.StatusOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
userFile := createUserFilename(input.Username)
|
||||||
|
if !storage.IsPresent(ctx, userFile) {
|
||||||
|
return nil, http.StatusUnauthorized, wrapAPIError(errors.New("Userfile not present"), "Authorization error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve data file
|
||||||
|
user, err := dataObjectFromStorage(ctx, storage, userFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, http.StatusInternalServerError, wrapAPIError(err, "Unable to retrieve data file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check bcrypt password and deprecated version of password
|
||||||
|
deprecatedPassword := fmt.Sprintf("%x", sha1.Sum([]byte(cfg.PasswordSalt+input.Password))) // Here for backwards compatibility
|
||||||
|
if bcrypt.CompareHashAndPassword([]byte(user.MetaData.Password), []byte(input.Password)) != nil &&
|
||||||
|
user.MetaData.Password != deprecatedPassword {
|
||||||
|
return nil, http.StatusUnauthorized, wrapAPIError(errors.New("Password mismatch"), "Authorization error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply migrations to data file automatically
|
||||||
|
if err := user.migrate(ctx, storage, userFile, input.Password); err != nil {
|
||||||
|
return nil, http.StatusInternalServerError, wrapAPIError(err, "Migrating the data file caused an error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set user as logged in
|
||||||
|
if user.MetaData.MFASecret == "" {
|
||||||
|
sess.Users[input.Username] = userStateLoggedin
|
||||||
|
} else {
|
||||||
|
secret, err := openssl.New().DecryptBytes(input.Password, []byte(user.MetaData.MFASecret), openssl.DigestSHA256Sum)
|
||||||
|
if err != nil {
|
||||||
|
return nil, http.StatusInternalServerError, wrapAPIError(err, "Could not decrypt MFA secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
sess.Users[input.Username] = userStateRequireMFA
|
||||||
|
sess.MFACache[input.Username] = string(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output, http.StatusOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiLogoutUser removes the given user from the list of logged in users
|
||||||
|
// and forgets the MFA secret in case it was still set.
|
||||||
|
func apiLogoutUser(ctx context.Context, res http.ResponseWriter, r *http.Request, sess *sessionData) (interface{}, int, error) {
|
||||||
|
var (
|
||||||
|
output = map[string]interface{}{"success": true}
|
||||||
|
user = mux.Vars(r)["user"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if _, ok := sess.MFACache[user]; ok {
|
||||||
|
delete(sess.MFACache, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := sess.Users[user]; ok {
|
||||||
|
delete(sess.Users, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output, http.StatusOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiRegister takes an username and two passwords, compares the
|
||||||
|
// passwords, ensures the username is not already taken and creates
|
||||||
|
// a new empty user file.
|
||||||
|
func apiRegister(ctx context.Context, res http.ResponseWriter, r *http.Request, sess *sessionData) (interface{}, int, error) {
|
||||||
|
var (
|
||||||
|
input = struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
CheckPassword string `json:"check_password"`
|
||||||
|
}{}
|
||||||
|
output = map[string]interface{}{"success": true}
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(input); err != nil {
|
||||||
|
return nil, http.StatusBadRequest, wrapAPIError(err, "Unable to decode request")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check input
|
||||||
|
if input.Username == "" || input.Password == "" || input.Password != input.CheckPassword {
|
||||||
|
return nil, http.StatusBadRequest, wrapAPIError(errors.New("Invalid input data"), "Invalid input provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check username collision
|
||||||
|
userFile := createUserFilename(input.Username)
|
||||||
|
if storage.IsPresent(ctx, userFile) {
|
||||||
|
return nil, http.StatusBadRequest, wrapAPIError(errors.New("User file found"), "Username is already taken")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user file
|
||||||
|
user := newDataObject()
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, http.StatusInternalServerError, wrapAPIError(err, "Unable to generate bcrypt hash")
|
||||||
|
}
|
||||||
|
user.MetaData.Password = string(hashedPassword)
|
||||||
|
|
||||||
|
// Save the user file
|
||||||
|
if err := user.writeToStorage(ctx, storage, userFile); err != nil {
|
||||||
|
return nil, http.StatusInternalServerError, wrapAPIError(err, "Could not write data file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return output, http.StatusOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiSetUserData retrieves two checksums and an encrypted data blob.
|
||||||
|
// It compares the old checksum still matches to ensure no changes
|
||||||
|
// of another user is overwritten and it compares the received checksum
|
||||||
|
// of the new data blob to ensure the data integrity is fine. Afterwards
|
||||||
|
// the user file is backupped and updated.
|
||||||
|
func apiSetUserData(ctx context.Context, res http.ResponseWriter, r *http.Request, sess *sessionData) (interface{}, int, error) {
|
||||||
|
var (
|
||||||
|
input = struct {
|
||||||
|
Checksum string `json:"checksum"`
|
||||||
|
OldChecksum string `json:"old_checksum"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}{}
|
||||||
|
output = map[string]interface{}{"success": true}
|
||||||
|
username = mux.Vars(r)["user"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(input); err != nil {
|
||||||
|
return nil, http.StatusBadRequest, wrapAPIError(err, "Unable to decode request")
|
||||||
|
}
|
||||||
|
|
||||||
|
if state, ok := sess.Users[username]; !ok || state != userStateLoggedin {
|
||||||
|
return nil, http.StatusUnauthorized, wrapAPIError(errors.New("Access to user not logged in"), "Authorization error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve data file
|
||||||
|
userFile := createUserFilename(username)
|
||||||
|
user, err := dataObjectFromStorage(ctx, storage, userFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, http.StatusInternalServerError, wrapAPIError(err, "Unable to retrieve data file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the user is still updating the same version of the data
|
||||||
|
if user.MetaData.Version != input.OldChecksum {
|
||||||
|
return nil, http.StatusBadRequest, wrapAPIError(errors.New("Update on outdated data"), "Old data checksum does not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check we've got the data the user intended to send
|
||||||
|
if input.Checksum != fmt.Sprintf("%x", sha256.Sum256([]byte(input.Data))) {
|
||||||
|
return nil, http.StatusBadRequest, wrapAPIError(errors.New("Checksum mismatch on input data"), "New data checksum does not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a backup because you know...
|
||||||
|
if err := storage.Backup(ctx, userFile); err != nil {
|
||||||
|
return nil, http.StatusInternalServerError, wrapAPIError(err, "Could not create backup, nothin saved")
|
||||||
|
}
|
||||||
|
|
||||||
|
user.MetaData.Version = input.Checksum
|
||||||
|
user.Data = input.Data
|
||||||
|
|
||||||
|
if err := user.writeToStorage(ctx, storage, userFile); err != nil {
|
||||||
|
return nil, http.StatusInternalServerError, wrapAPIError(err, "Could not write data file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return output, http.StatusOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiSetUserSettings(ctx context.Context, res http.ResponseWriter, r *http.Request, sess *sessionData) (interface{}, int, error) {
|
||||||
|
// FIXME (kahlers): Implement this
|
||||||
|
return nil, http.StatusInternalServerError, wrapAPIError(errors.New("Not implemented yet"), "Not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiValidateMFA retrieves an OTP token and in case of a match with
|
||||||
|
// the token generated from the stored secret the user is logged in.
|
||||||
|
func apiValidateMFA(ctx context.Context, res http.ResponseWriter, r *http.Request, sess *sessionData) (interface{}, int, error) {
|
||||||
|
var (
|
||||||
|
input = struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}{}
|
||||||
|
output = map[string]interface{}{"success": true}
|
||||||
|
user = mux.Vars(r)["user"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(input); err != nil {
|
||||||
|
return nil, http.StatusBadRequest, wrapAPIError(err, "Unable to decode login data")
|
||||||
|
}
|
||||||
|
|
||||||
|
state, ok := sess.Users[user]
|
||||||
|
if ok && state != userStateLoggedin {
|
||||||
|
// Not requesting MFA authorization
|
||||||
|
return output, http.StatusOK, nil
|
||||||
|
} else if !ok {
|
||||||
|
return nil, http.StatusUnauthorized, wrapAPIError(errors.New("User not logged in"), "Authorization error")
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, ok := sess.MFACache[user]
|
||||||
|
if !ok {
|
||||||
|
return nil, http.StatusInternalServerError, wrapAPIError(errors.New("Missing OTP secret"), "Unable to find OTP secret")
|
||||||
|
}
|
||||||
|
if !totp.Validate(input.Token, secret) {
|
||||||
|
return nil, http.StatusUnauthorized, wrapAPIError(errors.New("OTP token mismatch"), "Invalid OTP token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove secret from session and set user logged in
|
||||||
|
delete(sess.MFACache, "user")
|
||||||
|
sess.Users[user] = userStateLoggedin
|
||||||
|
|
||||||
|
return output, http.StatusOK, nil
|
||||||
|
}
|
|
@ -2,11 +2,17 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const currentSchemaVersion = 1
|
||||||
|
|
||||||
type authorizedAccounts []authorizedAccount
|
type authorizedAccounts []authorizedAccount
|
||||||
type authorizedAccount struct {
|
type authorizedAccount struct {
|
||||||
Name string
|
Name string
|
||||||
|
@ -18,19 +24,66 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
type dataObject struct {
|
type dataObject struct {
|
||||||
|
SchemaVersion int `json:"schema_version"`
|
||||||
MetaData struct {
|
MetaData struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
MFASecret string `json:"mfa_secret"`
|
||||||
} `json:"metadata"`
|
} `json:"metadata"`
|
||||||
Data string `json:"data"`
|
Data string `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func readDataObject(in io.Reader) (*dataObject, error) {
|
func newDataObject() *dataObject {
|
||||||
t := &dataObject{}
|
return &dataObject{
|
||||||
return t, json.NewDecoder(in).Decode(t)
|
SchemaVersion: currentSchemaVersion,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dataObjectFromStorage(ctx context.Context, storage storageAdapter, filename string) (*dataObject, error) {
|
||||||
|
userFileRaw, err := storage.Read(ctx, filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Unable to read data file from storage")
|
||||||
|
}
|
||||||
|
t := &dataObject{}
|
||||||
|
return t, json.NewDecoder(userFileRaw).Decode(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME (kahlers): remove
|
||||||
func (d *dataObject) GetData() (io.Reader, error) {
|
func (d *dataObject) GetData() (io.Reader, error) {
|
||||||
buf := bytes.NewBuffer([]byte{})
|
buf := bytes.NewBuffer([]byte{})
|
||||||
return buf, json.NewEncoder(buf).Encode(d)
|
return buf, json.NewEncoder(buf).Encode(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *dataObject) migrate(ctx context.Context, storage storageAdapter, filename, password string) error {
|
||||||
|
needsMigrate := true
|
||||||
|
|
||||||
|
for needsMigrate {
|
||||||
|
switch d.SchemaVersion {
|
||||||
|
|
||||||
|
case 0: // Initial data file created before v2.0.0
|
||||||
|
// Ensure a bcrypt hashed password
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Unable to generate bcrypt hash")
|
||||||
|
}
|
||||||
|
d.MetaData.Password = string(hashedPassword)
|
||||||
|
|
||||||
|
default: // No migration for this schema version defined, everything fine
|
||||||
|
needsMigrate = false
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increase schema version, see if there are more migrates
|
||||||
|
d.SchemaVersion++
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.writeToStorage(ctx, storage, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d dataObject) writeToStorage(ctx context.Context, storage storageAdapter, filename string) error {
|
||||||
|
buf := bytes.NewBuffer([]byte{})
|
||||||
|
if err := json.NewEncoder(buf).Encode(d); err != nil {
|
||||||
|
return errors.Wrap(err, "Unable to marshal data object")
|
||||||
|
}
|
||||||
|
return errors.Wrap(storage.Write(ctx, filename, buf), "Unable to write data file to storage")
|
||||||
|
}
|
||||||
|
|
46
gzip.go
Normal file
46
gzip.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Gzip Compression
|
||||||
|
type gzipResponseWriter struct {
|
||||||
|
io.Writer
|
||||||
|
http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
return w.Writer.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func gzipHandler(handler http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
gz := gzip.NewWriter(w)
|
||||||
|
defer gz.Close()
|
||||||
|
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
||||||
|
handler.ServeHTTP(gzw, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func gzipFunc(f http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
|
f(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
gz := gzip.NewWriter(w)
|
||||||
|
defer gz.Close()
|
||||||
|
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
||||||
|
f(gzw, r)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,64 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/flosch/pongo2"
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type httpHelperFunc func(c context.Context, res http.ResponseWriter, r *http.Request, session *sessions.Session, ctx *pongo2.Context) (*string, error)
|
|
||||||
|
|
||||||
func httpHelper(f httpHelperFunc) http.HandlerFunc {
|
|
||||||
return func(res http.ResponseWriter, r *http.Request) {
|
|
||||||
sess, _ := cookieStore.Get(r, "cloudkeys-go")
|
|
||||||
ctx := pongo2.Context{}
|
|
||||||
|
|
||||||
if errFlash := sess.Flashes("error"); len(errFlash) > 0 {
|
|
||||||
ctx["error"] = errFlash[0].(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := getContext(r)
|
|
||||||
|
|
||||||
template, err := f(c, res, r, sess, &ctx)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(res, "An error ocurred.", http.StatusInternalServerError)
|
|
||||||
log.WithError(err).Error("Unable to execute template")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if template != nil {
|
|
||||||
ts := pongo2.NewSet("frontend", pongo2.MustNewLocalFileSystemLoader("templates"))
|
|
||||||
tpl, err := ts.FromFile(*template)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).WithFields(log.Fields{
|
|
||||||
"template": *template,
|
|
||||||
}).Error("Could not parse template")
|
|
||||||
http.Error(res, "An error ocurred.", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
out, err := tpl.Execute(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).WithFields(log.Fields{
|
|
||||||
"template": *template,
|
|
||||||
}).Error("Could not execute template")
|
|
||||||
http.Error(res, "An error ocurred.", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Write([]byte(out))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func simpleTemplateOutput(template string) httpHelperFunc {
|
|
||||||
return func(c context.Context, res http.ResponseWriter, r *http.Request, session *sessions.Session, ctx *pongo2.Context) (*string, error) {
|
|
||||||
return &template, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringPointer(s string) *string {
|
|
||||||
return &s
|
|
||||||
}
|
|
96
login.go
96
login.go
|
@ -1,96 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/sha1"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
|
|
||||||
"github.com/flosch/pongo2"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func loginHandler(c context.Context, res http.ResponseWriter, r *http.Request, session *sessions.Session, ctx *pongo2.Context) (*string, error) {
|
|
||||||
var (
|
|
||||||
username = strings.ToLower(r.FormValue("username"))
|
|
||||||
password = r.FormValue("password")
|
|
||||||
deprecatedPassword = fmt.Sprintf("%x", sha1.Sum([]byte(cfg.PasswordSalt+r.FormValue("password")))) // Here for backwards compatibility
|
|
||||||
)
|
|
||||||
|
|
||||||
if !storage.IsPresent(c, createUserFilename(username)) {
|
|
||||||
(*ctx)["error"] = true
|
|
||||||
return stringPointer("login.html"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
userFileRaw, err := storage.Read(c, createUserFilename(username))
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("Unable to read user file")
|
|
||||||
(*ctx)["error"] = true
|
|
||||||
return stringPointer("login.html"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
userFile, _ := readDataObject(userFileRaw)
|
|
||||||
|
|
||||||
bcryptValidationError := bcrypt.CompareHashAndPassword([]byte(userFile.MetaData.Password), []byte(password))
|
|
||||||
if bcryptValidationError != nil && userFile.MetaData.Password != deprecatedPassword {
|
|
||||||
(*ctx)["error"] = true
|
|
||||||
return stringPointer("login.html"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
auth, ok := session.Values["authorizedAccounts"].(authorizedAccounts)
|
|
||||||
if !ok {
|
|
||||||
auth = authorizedAccounts{}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, v := range auth {
|
|
||||||
if v.Name == username {
|
|
||||||
http.Redirect(res, r, fmt.Sprintf("u/%d/overview", i), http.StatusFound)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auth = append(auth, authorizedAccount{
|
|
||||||
Name: username,
|
|
||||||
UserFile: createUserFilename(username),
|
|
||||||
})
|
|
||||||
|
|
||||||
session.Values["authorizedAccounts"] = auth
|
|
||||||
if err := session.Save(r, res); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(res, r, fmt.Sprintf("u/%d/overview", len(auth)-1), http.StatusFound)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func logoutHandler(c context.Context, res http.ResponseWriter, r *http.Request, session *sessions.Session, ctx *pongo2.Context) (*string, error) {
|
|
||||||
session.Values["authorizedAccounts"] = authorizedAccounts{}
|
|
||||||
session.Save(r, res)
|
|
||||||
http.Redirect(res, r, "overview", http.StatusFound)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkLogin(c context.Context, r *http.Request, session *sessions.Session) (*authorizedAccount, error) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
idx, err := strconv.ParseInt(vars["userIndex"], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
auth, ok := session.Values["authorizedAccounts"].(authorizedAccounts)
|
|
||||||
if !ok {
|
|
||||||
auth = authorizedAccounts{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(auth)-1 < int(idx) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &auth[idx], nil
|
|
||||||
}
|
|
32
overview.go
32
overview.go
|
@ -1,32 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/flosch/pongo2"
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
)
|
|
||||||
|
|
||||||
func overviewHandler(c context.Context, res http.ResponseWriter, r *http.Request, session *sessions.Session, ctx *pongo2.Context) (*string, error) {
|
|
||||||
user, _ := checkLogin(c, r, session)
|
|
||||||
|
|
||||||
if user == nil || !storage.IsPresent(c, user.UserFile) {
|
|
||||||
http.Redirect(res, r, "../../login", http.StatusFound)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
frontendAccounts := []string{}
|
|
||||||
idx := -1
|
|
||||||
for i, v := range session.Values["authorizedAccounts"].(authorizedAccounts) {
|
|
||||||
frontendAccounts = append(frontendAccounts, v.Name)
|
|
||||||
if v.Name == user.Name {
|
|
||||||
idx = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(*ctx)["authorized_accounts"] = frontendAccounts
|
|
||||||
(*ctx)["current_user_index"] = idx
|
|
||||||
|
|
||||||
return stringPointer("overview.html"), nil
|
|
||||||
}
|
|
50
register.go
50
register.go
|
@ -1,50 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
|
|
||||||
"github.com/flosch/pongo2"
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func registerHandler(c context.Context, res http.ResponseWriter, r *http.Request, session *sessions.Session, ctx *pongo2.Context) (*string, error) {
|
|
||||||
var (
|
|
||||||
username = strings.ToLower(r.FormValue("username"))
|
|
||||||
password = r.FormValue("password")
|
|
||||||
passwordCheck = r.FormValue("password_repeat")
|
|
||||||
)
|
|
||||||
|
|
||||||
if username == "" || password == "" || password != passwordCheck {
|
|
||||||
return stringPointer("register.html"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if storage.IsPresent(c, createUserFilename(username)) {
|
|
||||||
(*ctx)["exists"] = true
|
|
||||||
return stringPointer("register.html"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("Could not hash user password")
|
|
||||||
(*ctx)["error"] = true
|
|
||||||
return stringPointer("register.html"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
d := dataObject{}
|
|
||||||
d.MetaData.Password = string(hashedPassword)
|
|
||||||
data, _ := d.GetData()
|
|
||||||
|
|
||||||
if err := storage.Write(c, createUserFilename(username), data); err != nil {
|
|
||||||
log.WithError(err).Error("Could not write user file to storage")
|
|
||||||
(*ctx)["error"] = true
|
|
||||||
return stringPointer("register.html"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
(*ctx)["created"] = true
|
|
||||||
return stringPointer("register.html"), nil
|
|
||||||
}
|
|
45
router.go
45
router.go
|
@ -8,36 +8,23 @@ import (
|
||||||
|
|
||||||
func router() *mux.Router {
|
func router() *mux.Router {
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
r.PathPrefix("/assets/").HandlerFunc(serveAssets)
|
r.PathPrefix("/assets/").HandlerFunc(gzipFunc(serveAssets))
|
||||||
|
|
||||||
r.HandleFunc("/register", httpHelper(simpleTemplateOutput("register.html"))).
|
registerAPIv2(r.PathPrefix("/v2").Subrouter())
|
||||||
Methods("GET")
|
|
||||||
r.HandleFunc("/register", httpHelper(registerHandler)).
|
|
||||||
Methods("POST")
|
|
||||||
|
|
||||||
r.HandleFunc("/login", httpHelper(simpleTemplateOutput("login.html"))).
|
|
||||||
Methods("GET")
|
|
||||||
r.HandleFunc("/login", httpHelper(loginHandler)).
|
|
||||||
Methods("POST")
|
|
||||||
|
|
||||||
r.HandleFunc("/logout", httpHelper(logoutHandler)).
|
|
||||||
Methods("GET")
|
|
||||||
|
|
||||||
r.HandleFunc("/u/{userIndex:[0-9]+}/overview", httpHelper(overviewHandler)).
|
|
||||||
Methods("GET")
|
|
||||||
|
|
||||||
r.HandleFunc("/u/{userIndex:[0-9]+}/ajax", httpHelper(ajaxGetHandler)).
|
|
||||||
Methods("GET")
|
|
||||||
r.HandleFunc("/u/{userIndex:[0-9]+}/ajax", httpHelper(ajaxPostHandler)).
|
|
||||||
Methods("POST")
|
|
||||||
|
|
||||||
/* --- SUPPORT FOR DEPRECATED METHODS --- */
|
|
||||||
r.HandleFunc("/", func(res http.ResponseWriter, r *http.Request) {
|
|
||||||
http.Redirect(res, r, "u/0/overview", http.StatusFound)
|
|
||||||
}).Methods("GET")
|
|
||||||
r.HandleFunc("/overview", func(res http.ResponseWriter, r *http.Request) {
|
|
||||||
http.Redirect(res, r, "u/0/overview", http.StatusFound)
|
|
||||||
}).Methods("GET")
|
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registerAPIv2(r *mux.Router) {
|
||||||
|
r.HandleFunc("/login", apiHelper(apiLogin)).Methods(http.MethodPost)
|
||||||
|
r.HandleFunc("/register", apiHelper(apiRegister)).Methods(http.MethodPost)
|
||||||
|
r.HandleFunc("/users", apiHelper(apiListUsers)).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
r.HandleFunc("/user/{user}/data", apiHelper(apiGetUserData)).Methods(http.MethodGet)
|
||||||
|
r.HandleFunc("/user/{user}/data", apiHelper(apiSetUserData)).Methods(http.MethodPut)
|
||||||
|
r.HandleFunc("/user/{user}/logout", apiHelper(apiLogoutUser)).Methods(http.MethodPost)
|
||||||
|
r.HandleFunc("/user/{user}/settings", apiHelper(apiGetUserSettings)).Methods(http.MethodGet)
|
||||||
|
r.HandleFunc("/user/{user}/settings", apiHelper(apiSetUserSettings)).Methods(http.MethodPatch)
|
||||||
|
r.HandleFunc("/user/{user}/password", apiHelper(apiChangeLoginPassword)).Methods(http.MethodPut)
|
||||||
|
r.HandleFunc("/user/{user}/validate-mfa", apiHelper(apiValidateMFA)).Methods(http.MethodPost)
|
||||||
|
}
|
||||||
|
|
17
sessionData.go
Normal file
17
sessionData.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
type sessionData struct {
|
||||||
|
// The current state of all logged in users
|
||||||
|
Users map[string]userState
|
||||||
|
// MFA secrets are encrypted with users password, we need to
|
||||||
|
// store them inside the encrypted session until the user verified
|
||||||
|
// themselves
|
||||||
|
MFACache map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSessionData() *sessionData {
|
||||||
|
return &sessionData{
|
||||||
|
Users: make(map[string]userState),
|
||||||
|
MFACache: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ type storageAdapter interface {
|
||||||
IsPresent(ctx context.Context, identifier string) bool
|
IsPresent(ctx context.Context, identifier string) bool
|
||||||
Backup(ctx context.Context, identifier string) error
|
Backup(ctx context.Context, identifier string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type storageAdapterInitializer func(*url.URL) (storageAdapter, error)
|
type storageAdapterInitializer func(*url.URL) (storageAdapter, error)
|
||||||
|
|
||||||
func getStorageAdapter(cfg *config) (storageAdapter, error) {
|
func getStorageAdapter(cfg *config) (storageAdapter, error) {
|
||||||
|
|
Loading…
Reference in a new issue