all repos — flounder @ sftp

A small site builder for the Gemini protocol

db.go (view raw)

  1package main
  2
  3import (
  4	"crypto/rand"
  5	"database/sql"
  6	"fmt"
  7	"golang.org/x/crypto/bcrypt"
  8	"io"
  9	"io/ioutil"
 10	"log"
 11	"os"
 12	"path"
 13	"path/filepath"
 14	"sort"
 15	"strings"
 16	"time"
 17)
 18
 19var DB *sql.DB
 20
 21func initializeDB() {
 22	var err error
 23	DB, err = sql.Open("sqlite3", c.DBFile)
 24	if err != nil {
 25		log.Fatal(err)
 26	}
 27	createTablesIfDNE()
 28}
 29
 30// returns nil if login OK, err otherwise
 31// log in with email or username
 32func checkLogin(name string, password string) (string, bool, error) {
 33	row := DB.QueryRow("SELECT username, password_hash, active, admin FROM user where username = $1 OR email = $1", name)
 34	var db_password []byte
 35	var username string
 36	var active bool
 37	var isAdmin bool
 38	err := row.Scan(&username, &db_password, &active, &isAdmin)
 39	if err != nil {
 40		if strings.Contains(err.Error(), "no rows") {
 41			return username, isAdmin, fmt.Errorf("Username or email '" + name + "' does not exist")
 42		} else {
 43			return username, isAdmin, err
 44		}
 45	}
 46	if db_password != nil && !active {
 47		return username, isAdmin, fmt.Errorf("Your account is not active yet. Pending admin approval", c)
 48	}
 49	if bcrypt.CompareHashAndPassword(db_password, []byte(password)) == nil {
 50		return username, isAdmin, nil
 51	} else {
 52		return username, isAdmin, fmt.Errorf("Invalid password")
 53	}
 54}
 55
 56func getAnalyticsDB() (*sql.DB, error) {
 57	db, err := sql.Open("sqlite3", c.AnalyticsDBFile)
 58	_, err = db.Exec(`CREATE TABLE IF NOT EXISTS log (
 59  id INTEGER PRIMARY KEY NOT NULL,
 60  timestamp TEXT NOT NULL,
 61  protocol TEXT NOT NULL,
 62  request_ip TEXT,
 63  request_user TEXT,
 64  status INTEGER,
 65  destination_host TEXT,
 66  path TEXT,
 67  method TEXT,
 68  referer TEXT
 69);`)
 70	return db, err
 71}
 72
 73type File struct { // also folders
 74	Creator     string
 75	Name        string // includes folder
 76	UpdatedTime time.Time
 77	TimeAgo     string
 78	IsText      bool
 79	Children    []File
 80	Host        string
 81}
 82
 83func fileFromPath(fullPath string) File {
 84	info, _ := os.Stat(fullPath)
 85	creatorFolder := getCreator(fullPath)
 86	isText := isTextFile(fullPath)
 87	updatedTime := info.ModTime()
 88	return File{
 89		Name:        getLocalPath(fullPath),
 90		Creator:     path.Base(creatorFolder),
 91		UpdatedTime: updatedTime,
 92		IsText:      isText,
 93		TimeAgo:     timeago(&updatedTime),
 94		Host:        c.Host,
 95	}
 96
 97}
 98
 99type User struct {
100	Username      string
101	Email         string
102	Active        bool
103	Admin         bool
104	CreatedAt     int64 // timestamp
105	Reference     string
106	Domain        string
107	DomainEnabled bool
108}
109
110func getActiveUserNames() ([]string, error) {
111	rows, err := DB.Query(`SELECT username from user WHERE active is true order by username`)
112	if err != nil {
113		return nil, err
114	}
115	var users []string
116	for rows.Next() {
117		var user string
118		err = rows.Scan(&user)
119		if err != nil {
120			return nil, err
121		}
122		users = append(users, user)
123	}
124
125	return users, nil
126}
127
128var domains map[string]string
129
130func refreshDomainMap() error {
131	domains = make(map[string]string)
132	rows, err := DB.Query(`SELECT domain, username from user WHERE domain != ""`)
133	if err != nil {
134		log.Println(err)
135		return err
136	}
137	for rows.Next() {
138		var domain string
139		var username string
140		err = rows.Scan(&domain, &username)
141		if err != nil {
142			return err
143		}
144		domains[domain] = username
145	}
146	return nil
147}
148
149func getUserByName(username string) (*User, error) {
150	var user User
151	row := DB.QueryRow(`SELECT username, email, active, admin, created_at, reference, domain, domain_enabled from user WHERE username = ?`, username)
152	err := row.Scan(&user.Username, &user.Email, &user.Active, &user.Admin, &user.CreatedAt, &user.Reference, &user.Domain, &user.DomainEnabled)
153	if err != nil {
154		return nil, err
155	}
156	return &user, nil
157}
158
159func getUsers() ([]User, error) {
160	rows, err := DB.Query(`SELECT username, email, active, admin, created_at, reference, domain from user ORDER BY created_at DESC`)
161	if err != nil {
162		return nil, err
163	}
164	var users []User
165	for rows.Next() {
166		var user User
167		err = rows.Scan(&user.Username, &user.Email, &user.Active, &user.Admin, &user.CreatedAt, &user.Reference, &user.Domain)
168		if err != nil {
169			return nil, err
170		}
171		users = append(users, user)
172	}
173	return users, nil
174}
175
176func getIndexFiles(admin bool) ([]*File, error) { // cache this function
177	result := []*File{}
178	err := filepath.Walk(c.FilesDirectory, func(thepath string, info os.FileInfo, err error) error {
179		if err != nil {
180			log.Printf("Failure accessing a path %q: %v\n", thepath, err)
181			return err // think about
182		}
183		if !admin && info.IsDir() && info.Name() == HiddenFolder {
184			return filepath.SkipDir
185		}
186		// make this do what it should
187		if !info.IsDir() {
188			res := fileFromPath(thepath)
189			result = append(result, &res)
190		}
191		return nil
192	})
193	if err != nil {
194		return nil, err
195	}
196	sort.Slice(result, func(i, j int) bool {
197		return result[i].UpdatedTime.After(result[j].UpdatedTime)
198	})
199	if len(result) > 50 {
200		result = result[:50]
201	}
202	return result, nil
203} // todo clean up paths
204
205func getMyFilesRecursive(p string, creator string) ([]File, error) {
206	result := []File{}
207	files, err := ioutil.ReadDir(p)
208	if err != nil {
209		return nil, err
210	}
211	for _, file := range files {
212		fullPath := path.Join(p, file.Name())
213		f := fileFromPath(fullPath)
214		if file.IsDir() {
215			f.Children, err = getMyFilesRecursive(path.Join(p, file.Name()), creator)
216		}
217		result = append(result, f)
218	}
219	return result, nil
220}
221
222func createTablesIfDNE() {
223	_, err := DB.Exec(`CREATE TABLE user (
224  id INTEGER PRIMARY KEY NOT NULL,
225  username TEXT NOT NULL UNIQUE,
226  email TEXT NOT NULL UNIQUE,
227  password_hash TEXT NOT NULL,
228  reference TEXT NOT NULL default "",
229  active boolean NOT NULL DEFAULT false,
230  admin boolean NOT NULL DEFAULT false,
231  created_at INTEGER DEFAULT (strftime('%s', 'now')),
232  domain TEXT NOT NULL default "",
233  domain_enabled BOOLEAN NOT NULL DEFAULT false
234);`)
235	if err == nil {
236		// on first creation, create admin user with pw admin
237		hashedPassword, err := bcrypt.GenerateFromPassword([]byte("admin"), 8) // TODO handle error
238		if err != nil {
239			log.Fatal(err)
240		}
241		_, err = DB.Exec(`INSERT OR IGNORE INTO user (username, email, password_hash, admin) values ('admin', 'default@flounder.local', ?, true)`, hashedPassword)
242		activateUser("admin")
243		if err != nil {
244			log.Fatal(err)
245		}
246	}
247
248	_, err = DB.Exec(`CREATE TABLE IF NOT EXISTS cookie_key (
249  value TEXT NOT NULL
250);`)
251	if err != nil {
252		log.Fatal(err)
253	}
254}
255
256// Generate a cryptographically secure key for the cookie store
257func generateCookieKeyIfDNE() []byte {
258	rows, err := DB.Query("SELECT value FROM cookie_key LIMIT 1")
259	defer rows.Close()
260	if err != nil {
261		log.Fatal(err)
262	}
263	if rows.Next() {
264		var cookie []byte
265		err := rows.Scan(&cookie)
266		if err != nil {
267			log.Fatal(err)
268		}
269		return cookie
270	} else {
271		k := make([]byte, 32)
272		_, err := io.ReadFull(rand.Reader, k)
273		if err != nil {
274			log.Fatal(err)
275		}
276		_, err = DB.Exec("insert into cookie_key values (?)", k)
277		if err != nil {
278			log.Fatal(err)
279		}
280		return k
281	}
282}