package model
import (
. "github.com/mickael-kerjean/nuage/server/common"
"bytes"
"database/sql"
"encoding/json"
"fmt"
"github.com/mattn/go-sqlite3"
"golang.org/x/crypto/bcrypt"
"log"
"net/http"
"net/smtp"
"html/template"
"strings"
"time"
)
const PASSWORD_DUMMY = "{{PASSWORD}}"
type Proof struct {
Id string `json:"id"`
Key string `json:"key"`
Value string `json:"-"`
Message *string `json:"message,omitempty"`
Error *string `json:"error,omitempty"`
}
type Share struct {
Id string `json:"id"`
Backend string `json:"-"`
Auth string `json:"auth,omitempty"`
Path string `json:"path"`
Password *string `json:"password,omitempty"`
Users *string `json:"users,omitempty"`
Expire *int64 `json:"expire,omitempty"`
Url *string `json:"url,omitempty"`
CanShare bool `json:"can_share"`
CanManageOwn bool `json:"can_manage_own"`
CanRead bool `json:"can_read"`
CanWrite bool `json:"can_write"`
CanUpload bool `json:"can_upload"`
}
func NewShare(id string) Share {
return Share{
Id: id,
}
}
func (s Share) IsValid() (bool, error) {
if s.Expire != nil {
now := time.Now().UnixNano() / 1000000
if now > *s.Expire {
return false, NewError("Link has expired", 410)
}
}
return true, nil
}
func (s *Share) MarshalJSON() ([]byte, error) {
p := Share{
s.Id,
s.Backend,
"",
s.Path,
func(pass *string) *string{
if pass != nil {
return NewString(PASSWORD_DUMMY)
}
return nil
}(s.Password),
s.Users,
s.Expire,
s.Url,
s.CanShare,
s.CanManageOwn,
s.CanRead,
s.CanWrite,
s.CanUpload,
}
return json.Marshal(p)
}
func(s *Share) UnmarshallJSON(b []byte) error {
var tmp map[string]interface{}
if err := json.Unmarshal(b, &tmp); err != nil {
return err
}
for key, value := range tmp {
switch key {
case "password": s.Password = NewStringpFromInterface(value)
case "users": s.Users = NewStringpFromInterface(value)
case "expire": s.Expire = NewInt64pFromInterface(value)
case "url": s.Url = NewStringpFromInterface(value)
case "can_share": s.CanShare = NewBoolFromInterface(value)
case "can_manage_own": s.CanManageOwn = NewBoolFromInterface(value)
case "can_read": s.CanRead = NewBoolFromInterface(value)
case "can_write": s.CanWrite = NewBoolFromInterface(value)
case "can_upload": s.CanUpload = NewBoolFromInterface(value)
}
}
return nil
}
func ShareList(p *Share) ([]Share, error) {
stmt, err := DB.Prepare("SELECT id, related_path, params FROM Share WHERE related_backend = ? AND related_path LIKE ? || '%' ")
if err != nil {
return nil, err
}
rows, err := stmt.Query(p.Backend, p.Path)
if err != nil {
return nil, err
}
sharedFiles := []Share{}
for rows.Next() {
var a Share
var params []byte
rows.Scan(&a.Id, &a.Path, ¶ms)
json.Unmarshal(params, &a)
sharedFiles = append(sharedFiles, a)
}
rows.Close()
return sharedFiles, nil
}
func ShareGet(p *Share) error {
stmt, err := DB.Prepare("SELECT id, related_path, params, auth FROM share WHERE id = ?")
if err != nil {
return err
}
defer stmt.Close()
row := stmt.QueryRow(p.Id)
var str []byte
if err = row.Scan(&p.Id, &p.Path, &str, &p.Auth); err != nil {
if err == sql.ErrNoRows {
return NewError("Not Found", 404)
}
return err
}
json.Unmarshal(str, &p)
return nil
}
func ShareUpsert(p *Share) error {
if p.Password != nil {
if *p.Password == PASSWORD_DUMMY {
var copy Share
copy.Id = p.Id
ShareGet(©);
p.Password = copy.Password
} else {
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(*p.Password), bcrypt.DefaultCost)
p.Password = NewString(string(hashedPassword))
}
}
stmt, err := DB.Prepare("INSERT INTO Location(backend, path) VALUES($1, $2)")
if err != nil {
return err
}
_, err = stmt.Exec(p.Backend, p.Path)
if err != nil {
throw := true
if ferr, ok := err.(sqlite3.Error); ok == true && ferr.ExtendedCode == sqlite3.ErrConstraintPrimaryKey {
throw = false
}
if throw == true {
return err
}
}
stmt, err = DB.Prepare("INSERT INTO Share(id, related_backend, related_path, params, auth) VALUES($1, $2, $3, $4, $5) ON CONFLICT(id) DO UPDATE SET related_backend = $2, related_path = $3, params = $4")
if err != nil {
return err
}
j, _ := json.Marshal(&struct {
Password *string `json:"password,omitempty"`
Users *string `json:"users,omitempty"`
Expire *int64 `json:"expire,omitempty"`
Url *string `json:"url,omitempty"`
CanShare bool `json:"can_share"`
CanManageOwn bool `json:"can_manage_own"`
CanRead bool `json:"can_read"`
CanWrite bool `json:"can_write"`
CanUpload bool `json:"can_upload"`
}{
Password: p.Password,
Users: p.Users,
Expire: p.Expire,
Url: p.Url,
CanShare: p.CanShare,
CanManageOwn: p.CanManageOwn,
CanRead: p.CanRead,
CanWrite: p.CanWrite,
CanUpload: p.CanUpload,
})
_, err = stmt.Exec(p.Id, p.Backend, p.Path, j, p.Auth)
return err
}
func ShareDelete(p *Share) error {
stmt, err := DB.Prepare("DELETE FROM Share WHERE id = ? AND related_backend = ?")
if err != nil {
return err
}
_, err = stmt.Exec(p.Id, p.Backend)
return err
}
func ShareProofVerifier(ctx *App, s Share, proof Proof) (Proof, error) {
p := proof
if proof.Key == "password" {
if s.Password == nil {
return p, NewError("No password required", 400)
}
time.Sleep(1000 * time.Millisecond)
if err := bcrypt.CompareHashAndPassword([]byte(*s.Password), []byte(proof.Value)); err != nil {
return p, NewError("Invalid Password", 403)
}
p.Value = *s.Password
}
if proof.Key == "email" {
// find out if user is authorized
if s.Users == nil {
return p, NewError("Authentication not required", 400)
}
var user *string
for _, possibleUser := range strings.Split(*s.Users, ",") {
if proof.Value == strings.Trim(possibleUser, " ") {
user = &proof.Value
}
}
if user == nil {
time.Sleep(1000 * time.Millisecond)
return p, NewError("No access was provided", 400)
}
// prepare the verification code
stmt, err := DB.Prepare("INSERT INTO Verification(key, code) VALUES(?, ?)");
if err != nil {
return p, err
}
code := RandomString(4)
if _, err := stmt.Exec("email::" + proof.Value, code); err != nil {
return p, err
}
// Prepare message
var b bytes.Buffer
t := template.New("email")
t.Parse(TmplEmailVerification())
t.Execute(&b, struct{
Code string
}{code})
p.Key = "code"
p.Value = ""
p.Message = NewString("We've sent you a message with a verification code")
// Send email
addr := fmt.Sprintf("%s:%d", ctx.Config.Email.Server, ctx.Config.Email.Port)
mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
subject := "Subject: Your verification code\n"
msg := []byte(subject + mime + "\n" + b.String())
auth := smtp.PlainAuth("", ctx.Config.Email.Username, ctx.Config.Email.Password, ctx.Config.Email.Server)
if err := smtp.SendMail(addr, auth, ctx.Config.Email.From, []string{"mickael@kerjean.me"}, msg); err != nil {
log.Println("ERROR: ", err)
log.Println("Verification code: " + code)
return p, NewError("Couldn't send email", 500)
}
}
if proof.Key == "code" {
// find key for given code
stmt, err := DB.Prepare("SELECT key FROM Verification WHERE code = ? AND expire > datetime('now')")
if err != nil {
return p, NewError("Not found", 404)
}
row := stmt.QueryRow(proof.Value)
var key string
if err = row.Scan(&key); err != nil {
if err == sql.ErrNoRows {
stmt.Close()
p.Key = "email"
p.Value = ""
return p, NewError("Not found", 404)
}
stmt.Close()
return p, err
}
stmt.Close()
// cleanup current attempt so that it isn't used for malicious purpose
if stmt, err = DB.Prepare("DELETE FROM Verification WHERE code = ?"); err == nil {
stmt.Exec(proof.Value)
stmt.Close()
}
p.Key = "email"
p.Value = strings.TrimPrefix(key, "email::")
}
return p, nil
}
func ShareProofGetAlreadyVerified(req *http.Request, ctx *App) []Proof {
var p []Proof
var cookieValue string
c, _ := req.Cookie(COOKIE_NAME_PROOF)
if c == nil {
return p
}
cookieValue = c.Value
if len(cookieValue) > 500 {
return p
}
j, err := DecryptString(ctx.Config.General.SecretKey, cookieValue)
if err != nil {
return p
}
_ = json.Unmarshal([]byte(j), &p)
return p
}
func ShareProofGetRequired(s Share) []Proof {
var p []Proof
if s.Password != nil {
p = append(p, Proof{Key: "password", Value: *s.Password})
}
if s.Users != nil {
p = append(p, Proof{Key: "email", Value: *s.Users})
}
return p
}
func ShareProofCalculateRemainings(ref []Proof, mem []Proof) []Proof {
var remainingProof []Proof
for i := 0; i < len(ref); i++ {
keep := true
for j := 0; j < len(mem); j++ {
if shareProofAreEquivalent(ref[i], mem[j]) {
keep = false
break;
}
}
if keep {
remainingProof = append(remainingProof, ref[i])
}
}
return remainingProof
}
func shareProofAreEquivalent(ref Proof, p Proof) bool {
if ref.Key != p.Key {
return false
}
for _, chunk := range strings.Split(ref.Value, ",") {
chunk = strings.Trim(chunk, " ")
if p.Id == Hash(ref.Key + "::" + chunk) {
return true
}
}
return false
}
func TmplEmailVerification() string {
return `
Nuage code
| |
Your verification code is: {{.Code}}
|
|
|
|
`
}