mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-10-27 11:35:04 +08:00
656 lines
17 KiB
Go
656 lines
17 KiB
Go
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 `
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta name="viewport" content="width=device-width" />
|
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
|
<title>Verification code</title>
|
|
<style>
|
|
/* -------------------------------------
|
|
GLOBAL RESETS
|
|
------------------------------------- */
|
|
img {
|
|
border: none;
|
|
-ms-interpolation-mode: bicubic;
|
|
max-width: 100%; }
|
|
body {
|
|
background-color: #f6f6f6;
|
|
font-family: sans-serif;
|
|
-webkit-font-smoothing: antialiased;
|
|
font-size: 14px;
|
|
line-height: 1.4;
|
|
margin: 0;
|
|
padding: 0;
|
|
-ms-text-size-adjust: 100%;
|
|
-webkit-text-size-adjust: 100%; }
|
|
table {
|
|
border-collapse: separate;
|
|
mso-table-lspace: 0pt;
|
|
mso-table-rspace: 0pt;
|
|
width: 100%; }
|
|
table td {
|
|
font-family: sans-serif;
|
|
font-size: 14px;
|
|
vertical-align: top; }
|
|
/* -------------------------------------
|
|
BODY & CONTAINER
|
|
------------------------------------- */
|
|
.body {
|
|
background-color: #f6f6f6;
|
|
width: 100%; }
|
|
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
|
.container {
|
|
display: block;
|
|
Margin: 0 auto !important;
|
|
/* makes it centered */
|
|
max-width: 450px;
|
|
padding: 10px;
|
|
width: 580px; }
|
|
/* This should also be a block element, so that it will fill 100% of the .container */
|
|
.content {
|
|
box-sizing: border-box;
|
|
display: block;
|
|
Margin: 0 auto;
|
|
max-width: 450px;
|
|
padding: 10px; }
|
|
/* -------------------------------------
|
|
HEADER, FOOTER, MAIN
|
|
------------------------------------- */
|
|
.main {
|
|
background: #ffffff;
|
|
border-radius: 3px;
|
|
width: 100%; }
|
|
.wrapper {
|
|
box-sizing: border-box;
|
|
padding: 20px; }
|
|
.content-block {
|
|
padding-bottom: 10px;
|
|
padding-top: 10px;
|
|
}
|
|
.footer {
|
|
clear: both;
|
|
Margin-top: 10px;
|
|
text-align: center;
|
|
width: 100%; }
|
|
.footer td,
|
|
.footer p,
|
|
.footer span,
|
|
.footer a {
|
|
color: #999999;
|
|
font-size: 12px;
|
|
text-align: center; }
|
|
/* -------------------------------------
|
|
TYPOGRAPHY
|
|
------------------------------------- */
|
|
h1,
|
|
h2,
|
|
h3,
|
|
h4 {
|
|
color: #000000;
|
|
font-family: sans-serif;
|
|
font-weight: 400;
|
|
line-height: 1.4;
|
|
margin: 0;
|
|
margin-bottom: 30px; }
|
|
h1 {
|
|
font-size: 35px;
|
|
font-weight: 300;
|
|
text-align: center;
|
|
text-transform: capitalize; }
|
|
p,
|
|
ul,
|
|
ol {
|
|
font-family: sans-serif;
|
|
font-size: 14px;
|
|
font-weight: normal;
|
|
margin: 0;
|
|
margin-bottom: 15px; }
|
|
p li,
|
|
ul li,
|
|
ol li {
|
|
list-style-position: inside;
|
|
margin-left: 5px; }
|
|
a {
|
|
color: #3498db;
|
|
text-decoration: underline; }
|
|
/* -------------------------------------
|
|
BUTTONS
|
|
------------------------------------- */
|
|
.btn {
|
|
box-sizing: border-box;
|
|
width: 100%; }
|
|
.btn > tbody > tr > td {
|
|
padding-bottom: 15px; }
|
|
.btn table {
|
|
width: auto; }
|
|
.btn table td {
|
|
background-color: #ffffff;
|
|
border-radius: 5px;
|
|
text-align: center; }
|
|
.btn a {
|
|
background-color: #ffffff;
|
|
border: solid 1px #3498db;
|
|
border-radius: 5px;
|
|
box-sizing: border-box;
|
|
color: #3498db;
|
|
cursor: pointer;
|
|
display: inline-block;
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
margin: 0;
|
|
padding: 12px 25px;
|
|
text-decoration: none;
|
|
text-transform: capitalize; }
|
|
.btn-primary table td {
|
|
background-color: #3498db; }
|
|
.btn-primary a {
|
|
background-color: #3498db;
|
|
border-color: #3498db;
|
|
color: #ffffff; }
|
|
/* -------------------------------------
|
|
OTHER STYLES THAT MIGHT BE USEFUL
|
|
------------------------------------- */
|
|
.last {
|
|
margin-bottom: 0; }
|
|
.first {
|
|
margin-top: 0; }
|
|
.align-center {
|
|
text-align: center; }
|
|
.align-right {
|
|
text-align: right; }
|
|
.align-left {
|
|
text-align: left; }
|
|
.clear {
|
|
clear: both; }
|
|
.mt0 {
|
|
margin-top: 0; }
|
|
.mb0 {
|
|
margin-bottom: 0; }
|
|
.preheader {
|
|
color: transparent;
|
|
display: none;
|
|
height: 0;
|
|
max-height: 0;
|
|
max-width: 0;
|
|
opacity: 0;
|
|
overflow: hidden;
|
|
mso-hide: all;
|
|
visibility: hidden;
|
|
width: 0; }
|
|
.powered-by a {
|
|
text-decoration: none; }
|
|
hr {
|
|
border: 0;
|
|
border-bottom: 1px solid #f6f6f6;
|
|
Margin: 20px 0; }
|
|
/* -------------------------------------
|
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
|
------------------------------------- */
|
|
@media only screen and (max-width: 490px) {
|
|
table[class=body] h1 {
|
|
font-size: 28px !important;
|
|
margin-bottom: 10px !important; }
|
|
table[class=body] p,
|
|
table[class=body] ul,
|
|
table[class=body] ol,
|
|
table[class=body] td,
|
|
table[class=body] span,
|
|
table[class=body] a {
|
|
font-size: 16px !important; }
|
|
table[class=body] .wrapper,
|
|
table[class=body] .article {
|
|
padding: 10px !important; }
|
|
table[class=body] .content {
|
|
padding: 0 !important; }
|
|
table[class=body] .container {
|
|
padding: 0 !important;
|
|
width: 100% !important; }
|
|
table[class=body] .main {
|
|
border-left-width: 0 !important;
|
|
border-radius: 0 !important;
|
|
border-right-width: 0 !important; }
|
|
table[class=body] .btn table {
|
|
width: 100% !important; }
|
|
table[class=body] .btn a {
|
|
width: 100% !important; }
|
|
table[class=body] .img-responsive {
|
|
height: auto !important;
|
|
max-width: 100% !important;
|
|
width: auto !important; }}
|
|
/* -------------------------------------
|
|
PRESERVE THESE STYLES IN THE HEAD
|
|
------------------------------------- */
|
|
@media all {
|
|
.ExternalClass {
|
|
width: 100%; }
|
|
.ExternalClass,
|
|
.ExternalClass p,
|
|
.ExternalClass span,
|
|
.ExternalClass font,
|
|
.ExternalClass td,
|
|
.ExternalClass div {
|
|
line-height: 100%; }
|
|
.apple-link a {
|
|
color: inherit !important;
|
|
font-family: inherit !important;
|
|
font-size: inherit !important;
|
|
font-weight: inherit !important;
|
|
line-height: inherit !important;
|
|
text-decoration: none !important; }
|
|
.btn-primary table td:hover {
|
|
background-color: #34495e !important; }
|
|
.btn-primary a:hover {
|
|
background-color: #34495e !important;
|
|
border-color: #34495e !important; } }
|
|
</style>
|
|
</head>
|
|
<body class="">
|
|
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
|
<tr>
|
|
<td> </td>
|
|
<td class="container">
|
|
<div class="content">
|
|
|
|
<!-- START CENTERED WHITE CONTAINER -->
|
|
<span class="preheader">Your code to login</span>
|
|
<table class="main">
|
|
|
|
<!-- START MAIN CONTENT AREA -->
|
|
<tr>
|
|
<td class="wrapper">
|
|
<table border="0" cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td>
|
|
<h2 style="font-weight:100;margin:0">Your verification code is: <strong>{{.Code}}</strong></h2>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<div style="margin-top:10px;font-style:italic;font-size:0.9em;">When mounted as a network drive, you can authenticate as: {{.Username}}</div>
|
|
</td>
|
|
</tr>
|
|
|
|
<!-- END MAIN CONTENT AREA -->
|
|
</table>
|
|
|
|
<!-- START FOOTER -->
|
|
<div class="footer">
|
|
<table border="0" cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td class="content-block powered-by">
|
|
Powered by <a href="http://github.com/mickael-kerjean/filestash">Filestash</a>.
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
<!-- END FOOTER -->
|
|
|
|
<!-- END CENTERED WHITE CONTAINER -->
|
|
</div>
|
|
</td>
|
|
<td> </td>
|
|
</tr>
|
|
</table>
|
|
</body>
|
|
</html>
|
|
`
|
|
}
|
|
|
|
func networkDriveUsernameEnc(email string) string {
|
|
return email + "[" + Hash(email+SECRET_KEY_DERIVATE_FOR_HASH, 10) + "]"
|
|
}
|