Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions _vagrant/Vagrantfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
docker.vm.provider "virtualbox" do |v|
v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
v.customize ["modifyvm", :id, "--natdnsproxy1", "on"]
v.memory = 2048
end

docker.vm.provision "docker" do |d|
Expand Down
7 changes: 4 additions & 3 deletions circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ machine:
Europe/Berlin

dependencies:
cache_directories:
- /home/ubuntu/api/redis-2.8.18
pre:
- source /etc/lsb-release && echo "deb http://download.rethinkdb.com/apt $DISTRIB_CODENAME main" | sudo tee /etc/apt/sources.list.d/rethinkdb.list
- wget -qO- http://download.rethinkdb.com/apt/pubkey.gpg | sudo apt-key add -
- sudo apt-get update
- sudo apt-get install rethinkdb
- wget http://download.redis.io/releases/redis-2.8.18.tar.gz
- tar xvzf redis-2.8.18.tar.gz
- cd redis-2.8.18 && make
- if [[ ! -e redis-2.8.18/src/redis-server ]]; then wget http://download.redis.io/releases/redis-2.8.18.tar.gz && tar xvzf redis-2.8.18.tar.gz; fi
- if [[ ! -e redis-2.8.18/src/redis-server ]]; then cd redis-2.8.18 && make; fi
- go get github.com/apcera/gnatsd
post:
- rethinkdb --bind all:
Expand Down
21 changes: 21 additions & 0 deletions models/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package models
import (
"github.com/gyepisam/mcf"
_ "github.com/gyepisam/mcf/scrypt" // Required to have mcf hash the password into scrypt
"github.com/lavab/api/factor"
)

// Account stores essential data for a Lavaboom user, and is thus not encrypted.
Expand Down Expand Up @@ -81,6 +82,26 @@ func (a *Account) VerifyPassword(password string) (bool, bool, error) {
return true, false, nil
}

// Verify2FA verifies the 2FA token with the account settings.
// Returns verified, challenge, error
func (a *Account) Verify2FA(factor factor.Factor, token string) (bool, string, error) {
if token == "" {
req, err := factor.Request(a.ID)
if err != nil {
return false, "", err
}

return false, req, nil
}

ok, err := factor.Verify(a.FactorValue, token)
if err != nil {
return false, "", err
}

return ok, "", nil
}

// SettingsData TODO
type SettingsData struct {
}
Expand Down
1 change: 0 additions & 1 deletion models/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ type Key struct {
//Body []byte `json:"body" gorethink:"body"` // Raw key contents

Headers map[string]string `json:"headers" gorethink:"headers"` // Headers passed with the key
Email string `json:"email" gorethink:"email"` // Address associated with the key
Algorithm string `json:"algorithm" gorethink:"algorithm"` // Algorithm of the key
Length uint16 `json:"length" gorethink:"length"` // Length of the key
Key string `json:"key" gorethink:"key"` // Armor-encoded key
Expand Down
134 changes: 101 additions & 33 deletions routes/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package routes

import (
"net/http"
"time"

"github.com/Sirupsen/logrus"
"github.com/zenazn/goji/web"
Expand Down Expand Up @@ -82,6 +83,24 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) {
return
}

if input.Username != "" {
if used, err := env.Reservations.IsUsernameUsed(input.Username); err != nil || used {
utils.JSONResponse(w, 400, &AccountsCreateResponse{
Success: false,
Message: "Username already reserved",
})
return
}

if used, err := env.Accounts.IsUsernameUsed(input.Username); err != nil || used {
utils.JSONResponse(w, 400, &AccountsCreateResponse{
Success: false,
Message: "Username already used",
})
return
}
}

// Adding to [beta] queue
if requestType[:5] == "queue" {
if requestType[6:] == "reserve" {
Expand All @@ -93,22 +112,6 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) {
})
return
}

if used, err := env.Reservations.IsUsernameUsed(input.Username); err != nil || used {
utils.JSONResponse(w, 400, &AccountsCreateResponse{
Success: false,
Message: "Username already reserved",
})
return
}

if used, err := env.Accounts.IsUsernameUsed(input.Username); err != nil || used {
utils.JSONResponse(w, 400, &AccountsCreateResponse{
Success: false,
Message: "Username already used",
})
return
}
}

// Ensure that the email is not already used to reserve/register
Expand Down Expand Up @@ -205,15 +208,6 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) {

// TODO: sanitize user name (i.e. remove caps, periods)

// Both invited and classic require an unique username, so ensure that the user with requested username isn't already used
if _, err := env.Accounts.FindAccountByName(input.Username); err == nil {
utils.JSONResponse(w, 409, &AccountsCreateResponse{
Success: false,
Message: "Username already exists",
})
return
}

// Both username and password are filled, so we can create a new account.
account := &models.Account{
Resource: models.MakeResource("", input.Username),
Expand Down Expand Up @@ -332,16 +326,21 @@ func AccountsGet(c web.C, w http.ResponseWriter, r *http.Request) {

// AccountsUpdateRequest contains the input for the AccountsUpdate endpoint.
type AccountsUpdateRequest struct {
AltEmail string `json:"alt_email" schema:"alt_email"`
CurrentPassword string `json:"current_password" schema:"current_password"`
NewPassword string `json:"new_password" schema:"new_password"`
AltEmail string `json:"alt_email" schema:"alt_email"`
CurrentPassword string `json:"current_password" schema:"current_password"`
NewPassword string `json:"new_password" schema:"new_password"`
FactorType string `json:"factor_type" schema:"factor_type"`
FactorValue []string `json:"factor_value" schema:"factor_value"`
Token string `json:"token" schema:"token"`
}

// AccountsUpdateResponse contains the result of the AccountsUpdate request.
type AccountsUpdateResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Account *models.Account `json:"account"`
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Account *models.Account `json:"account,omitempty"`
FactorType string `json:"factor_type,omitempty"`
FactorChallenge string `json:"factor_challenge,omitempty"`
}

// AccountsUpdate allows changing the account's information (password etc.)
Expand All @@ -354,7 +353,7 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) {
"error": err,
}).Warn("Unable to decode a request")

utils.JSONResponse(w, 409, &AccountsUpdateResponse{
utils.JSONResponse(w, 400, &AccountsUpdateResponse{
Success: false,
Message: "Invalid input format",
})
Expand Down Expand Up @@ -387,13 +386,63 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) {
}

if valid, _, err := user.VerifyPassword(input.CurrentPassword); err != nil || !valid {
utils.JSONResponse(w, 409, &AccountsUpdateResponse{
utils.JSONResponse(w, 403, &AccountsUpdateResponse{
Success: false,
Message: "Invalid current password",
})
return
}

// Check for 2nd factor
if user.FactorType != "" {
factor, ok := env.Factors[user.FactorType]
if ok {
// Verify the 2FA
verified, challenge, err := user.Verify2FA(factor, input.Token)
if err != nil {
utils.JSONResponse(w, 500, &AccountsUpdateResponse{
Success: false,
Message: "Internal 2FA error",
})

env.Log.WithFields(logrus.Fields{
"err": err.Error(),
"factor": user.FactorType,
}).Warn("2FA authentication error")
return
}

// Token was probably empty. Return the challenge.
if !verified && challenge != "" {
utils.JSONResponse(w, 403, &AccountsUpdateResponse{
Success: false,
Message: "2FA token was not passed",
FactorType: user.FactorType,
FactorChallenge: challenge,
})
return
}

// Token was incorrect
if !verified {
utils.JSONResponse(w, 403, &AccountsUpdateResponse{
Success: false,
Message: "Invalid token passed",
FactorType: user.FactorType,
})
return
}
}
}

if input.NewPassword != "" && !utils.IsPasswordSecure(input.NewPassword) {
utils.JSONResponse(w, 400, &AccountsUpdateResponse{
Success: false,
Message: "Weak new password",
})
return
}

if input.NewPassword != "" {
err = user.SetPassword(input.NewPassword)
if err != nil {
Expand All @@ -413,6 +462,25 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) {
user.AltEmail = input.AltEmail
}

if input.FactorType != "" {
// Check if such factor exists
if _, exists := env.Factors[input.FactorType]; !exists {
utils.JSONResponse(w, 400, &AccountsUpdateResponse{
Success: false,
Message: "Invalid new 2FA type",
})
return
}

user.FactorType = input.FactorType
}

if input.FactorValue != nil && len(input.FactorValue) > 0 {
user.FactorValue = input.FactorValue
}

user.DateModified = time.Now()

err = env.Accounts.UpdateID(session.Owner, user)
if err != nil {
env.Log.WithFields(logrus.Fields{
Expand Down
6 changes: 3 additions & 3 deletions routes/accounts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ func TestAccountsCreateInvited(t *testing.T) {
require.Nil(t, err)

// Check the result's contents
require.True(t, response1.Success)
require.Equal(t, "A new account was successfully created", response1.Message)
require.True(t, response1.Success)
require.NotEmpty(t, response1.Account.ID)

accountID = response1.Account.ID
Expand Down Expand Up @@ -146,12 +146,12 @@ func TestAccountsCreateInvitedExisting(t *testing.T) {

// Check the result's contents
require.False(t, response.Success)
require.Equal(t, "Username already exists", response.Message)
require.Equal(t, "Username already used", response.Message)
}

func TestAccountsCreateInvitedWeakPassword(t *testing.T) {
const (
username = "jeremy"
username = "jeremylicious"
password = "c0067d4af4e87f00dbac63b6156828237059172d1bbeac67427345d6a9fda484"
)

Expand Down
86 changes: 86 additions & 0 deletions routes/avatars.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package routes

import (
"crypto/md5"
"encoding/hex"
"image/color"
"image/png"
"net/http"
"strconv"

"github.com/lavab/api/utils"

"github.com/cupcake/sigil/gen"
"github.com/zenazn/goji/web"
)

var avatarConfig = gen.Sigil{
Rows: 5,
Foreground: []color.NRGBA{
color.NRGBA{45, 79, 255, 255},
color.NRGBA{254, 180, 44, 255},
color.NRGBA{226, 121, 234, 255},
color.NRGBA{30, 179, 253, 255},
color.NRGBA{232, 77, 65, 255},
color.NRGBA{49, 203, 115, 255},
color.NRGBA{141, 69, 170, 255},
},
Background: color.NRGBA{224, 224, 224, 255},
}

func Avatars(c web.C, w http.ResponseWriter, r *http.Request) {
// Parse the query params
query := r.URL.Query()

// Settings
var (
widthString = query.Get("width")
width int
)

// Read width
if widthString == "" {
width = 100
} else {
var err error
width, err = strconv.Atoi(widthString)
if err != nil {
utils.JSONResponse(w, 400, map[string]interface{}{
"message": "Invalid width",
})
return
}
}

hash := c.URLParams["hash"]
ext := c.URLParams["ext"]

if ext != "png" && ext != "svg" {
ext = "png"
}

// data to parse
var data []byte

// md5 hash
if len(hash) == 32 {
data, _ = hex.DecodeString(hash)
}

// not md5
if data == nil {
hashed := md5.Sum([]byte(hash))
data = hashed[:]
}

// if svg
if ext == "svg" {
w.Header().Set("Content-Type", "image/svg+xml")
avatarConfig.MakeSVG(w, width, false, data)
return
}

// generate the png
w.Header().Set("Content-Type", "image/png")
png.Encode(w, avatarConfig.Make(width, false, data))
}
Loading