package model import ( "bytes" "crypto/tls" "database/sql" "encoding/json" . "github.com/mickael-kerjean/filestash/server/common" "golang.org/x/crypto/bcrypt" "gopkg.in/gomail.v2" "html/template" sqlite "modernc.org/sqlite" "net/http" "strings" "time" ) type Proof struct { Id string `json:"id"` Key string `json:"key"` Value string `json:"-"` Message *string `json:"message,omitempty"` Error *string `json:"error,omitempty"` } func ShareList(backend string, path string) ([]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(backend, 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 ShareAll() ([]Share, error) { rows, err := DB.Query("SELECT id, related_path, params FROM Share") 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(id string) (Share, error) { var p Share stmt, err := DB.Prepare("SELECT id, related_backend, related_path, auth, params FROM share WHERE id = ?") if err != nil { return p, err } defer stmt.Close() row := stmt.QueryRow(id) var str []byte if err = row.Scan(&p.Id, &p.Backend, &p.Path, &p.Auth, &str); err != nil { if err == sql.ErrNoRows { return p, ErrNotFound } return p, err } json.Unmarshal(str, &p) return p, nil } func ShareUpsert(p *Share) error { if p.Password != nil { if *p.Password == PASSWORD_DUMMY { if s, err := ShareGet(p.Id); err != nil { p.Password = s.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 errConstraintPrimaryKey := 1555 if ferr, ok := err.(*sqlite.Error); ok == true && ferr.Code() == 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(id string) error { stmt, err := DB.Prepare("DELETE FROM Share WHERE id = ?") if err != nil { return err } _, err = stmt.Exec(id) return err } func ShareProofVerifier(s Share, proof Proof) (Proof, error) { p := proof if proof.Key == "password" { if s.Password == nil { return p, NewError("No password required", 400) } v, ok := ShareProofVerifierPassword(*s.Password, proof.Value) if ok == false { time.Sleep(1000 * time.Millisecond) return p, ErrInvalidPassword } p.Value = v } if proof.Key == "email" { // find out if user is authorized if s.Users == nil { return p, NewError("Authentication not required", 400) } v, ok := ShareProofVerifierEmail(*s.Users, proof.Value) if ok == false { time.Sleep(1000 * time.Millisecond) return p, ErrNotAuthorized } user := v // 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::"+user, 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 Username string }{code, networkDriveUsernameEnc(user)}) p.Key = "code" p.Value = "" p.Message = NewString("We've sent you a message with a verification code") // Send email email := struct { Hostname string `json:"server"` Port int `json:"port"` Username string `json:"username"` Password string `json:"password"` From string `json:"from"` }{ Hostname: Config.Get("email.server").String(), Port: Config.Get("email.port").Int(), Username: Config.Get("email.username").String(), Password: Config.Get("email.password").String(), From: Config.Get("email.from").String(), } m := gomail.NewMessage() m.SetHeader("From", email.From) m.SetHeader("To", proof.Value) m.SetHeader("Subject", "Your verification code") m.SetBody("text/html", b.String()) d := gomail.NewDialer(email.Hostname, email.Port, email.Username, email.Password) d.TLSConfig = &tls.Config{InsecureSkipVerify: true} if err := d.DialAndSend(m); err != nil { Log.Error("Sendmail error: %v", err) Log.Error("Verification code '%s'", code) return p, NewError("Couldn't send email", 500) } return p, nil } 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 ShareProofVerifierPassword(hashed string, given string) (string, bool) { if err := bcrypt.CompareHashAndPassword([]byte(hashed), []byte(given)); err != nil { return "", false } return hashed, true } func ShareProofVerifierEmail(users string, wanted string) (string, bool) { s := strings.Split(users, ",") user := "" for _, possibleUser := range s { possibleUser := strings.Trim(possibleUser, " ") if wanted == possibleUser { user = possibleUser break } else if possibleUser[0:1] == "*" { if strings.HasSuffix(wanted, strings.TrimPrefix(possibleUser, "*")) { user = possibleUser break } } } if user == "" { return "", false } return user, true } func ShareProofGetAlreadyVerified(req *http.Request) []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(SECRET_KEY_DERIVATE_FOR_PROOF, 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 } else if ref.Value != "" && ref.Value == p.Value { return true } for _, chunk := range strings.Split(ref.Value, ",") { chunk = strings.Trim(chunk, " ") if p.Id == Hash(ref.Key+"::"+chunk, 20) { return true } } return false } func TmplEmailVerification() string { return ` Verification code
 
Your code to login

Your verification code is: {{.Code}}

When mounted as a network drive, you can authenticate as: {{.Username}}
 
` } func networkDriveUsernameEnc(email string) string { return email + "[" + Hash(email+SECRET_KEY_DERIVATE_FOR_HASH, 10) + "]" }