1
0
mirror of https://github.com/Luzifer/cloudkeys-go.git synced 2024-09-19 15:42:58 +00:00

Reimplement Go server as API-Server

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2018-11-04 11:50:03 +01:00
parent ed73116f3a
commit b4793cbab5
Signed by: luzifer
GPG Key ID: DC2729FDD34BE99E
12 changed files with 597 additions and 384 deletions

107
ajax.go
View File

@ -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
View 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
View 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
}

View File

@ -2,11 +2,17 @@ package main
import (
"bytes"
"context"
"encoding/gob"
"encoding/json"
"io"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
)
const currentSchemaVersion = 1
type authorizedAccounts []authorizedAccount
type authorizedAccount struct {
Name string
@ -18,19 +24,66 @@ func init() {
}
type dataObject struct {
SchemaVersion int `json:"schema_version"`
MetaData struct {
Version string `json:"version"`
Password string `json:"password"`
MFASecret string `json:"mfa_secret"`
} `json:"metadata"`
Data string `json:"data"`
}
func readDataObject(in io.Reader) (*dataObject, error) {
t := &dataObject{}
return t, json.NewDecoder(in).Decode(t)
func newDataObject() *dataObject {
return &dataObject{
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) {
buf := bytes.NewBuffer([]byte{})
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
View 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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -8,36 +8,23 @@ import (
func router() *mux.Router {
r := mux.NewRouter()
r.PathPrefix("/assets/").HandlerFunc(serveAssets)
r.PathPrefix("/assets/").HandlerFunc(gzipFunc(serveAssets))
r.HandleFunc("/register", httpHelper(simpleTemplateOutput("register.html"))).
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")
registerAPIv2(r.PathPrefix("/v2").Subrouter())
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
View 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),
}
}

View File

@ -17,6 +17,7 @@ type storageAdapter interface {
IsPresent(ctx context.Context, identifier string) bool
Backup(ctx context.Context, identifier string) error
}
type storageAdapterInitializer func(*url.URL) (storageAdapter, error)
func getStorageAdapter(cfg *config) (storageAdapter, error) {