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 code to login

Your verification code is: {{.Code}}

 
` }