all repos — flounder @ 574cd34fd855e31a22511e226d26b9d25be4d99c

A small site builder for the Gemini protocol

main.go (view raw)

  1package main
  2
  3import (
  4	"crypto/rand"
  5	"database/sql"
  6	"flag"
  7	"fmt"
  8	"github.com/gorilla/sessions"
  9	"io"
 10	"io/ioutil"
 11	"log"
 12	mathrand "math/rand"
 13	"mime"
 14	"os"
 15	"path"
 16	"path/filepath"
 17	"sort"
 18	"strings"
 19	"sync"
 20	"time"
 21)
 22
 23var c Config // global var to hold static configuration
 24
 25type File struct { // also folders
 26	Creator     string
 27	Name        string
 28	UpdatedTime time.Time
 29	TimeAgo     string
 30	IsText      bool
 31	Children    []*File
 32}
 33
 34type User struct {
 35	Username  string
 36	Email     string
 37	Active    bool
 38	Admin     bool
 39	CreatedAt int // timestamp
 40}
 41
 42// returns in a random order
 43func getActiveUserNames() ([]string, error) {
 44	rows, err := DB.Query(`SELECT username from user WHERE active is true`)
 45	if err != nil {
 46		return nil, err
 47	}
 48	var users []string
 49	for rows.Next() {
 50		var user string
 51		err = rows.Scan(&user)
 52		if err != nil {
 53			return nil, err
 54		}
 55		users = append(users, user)
 56	}
 57
 58	dest := make([]string, len(users))
 59	perm := mathrand.Perm(len(users))
 60	for i, v := range perm {
 61		dest[v] = users[i]
 62	}
 63	return dest, nil
 64}
 65
 66func getUsers() ([]User, error) {
 67	rows, err := DB.Query(`SELECT username, email, active, admin, created_at from user ORDER BY created_at DESC`)
 68	if err != nil {
 69		return nil, err
 70	}
 71	var users []User
 72	for rows.Next() {
 73		var user User
 74		err = rows.Scan(&user.Username, &user.Email, &user.Active, &user.Admin, &user.CreatedAt)
 75		if err != nil {
 76			return nil, err
 77		}
 78		users = append(users, user)
 79	}
 80	return users, nil
 81}
 82
 83func getIndexFiles() ([]*File, error) { // cache this function
 84	result := []*File{}
 85	err := filepath.Walk(c.FilesDirectory, func(thepath string, info os.FileInfo, err error) error {
 86		if err != nil {
 87			log.Printf("Failure accessing a path %q: %v\n", thepath, err)
 88			return err // think about
 89		}
 90		// make this do what it should
 91		if !info.IsDir() {
 92			creatorFolder := strings.Split(thepath, "/")[1]
 93			updatedTime := info.ModTime()
 94			result = append(result, &File{
 95				Name:        info.Name(),
 96				Creator:     path.Base(creatorFolder),
 97				UpdatedTime: updatedTime,
 98				TimeAgo:     timeago(&updatedTime),
 99			})
100		}
101		return nil
102	})
103	if err != nil {
104		return nil, err
105	}
106	sort.Slice(result, func(i, j int) bool {
107		return result[i].UpdatedTime.After(result[j].UpdatedTime)
108	})
109	if len(result) > 50 {
110		result = result[:50]
111	}
112	return result, nil
113} // todo clean up paths
114
115func getFiles(p string) ([]*File, error) {
116	result := []*File{}
117	files, err := ioutil.ReadDir(p)
118	if err != nil {
119		return nil, err
120	}
121	for _, file := range files {
122		isText := strings.HasPrefix(mime.TypeByExtension(path.Ext(file.Name())), "text")
123		f := &File{
124			Name: file.Name(),
125			// Creator:     strings.Split(p, "/")[0],
126			UpdatedTime: file.ModTime(),
127			IsText:      isText,
128		}
129		if file.IsDir() {
130			f.Children, err = getFiles(path.Join(p, f.Name))
131		}
132		result = append(result, f)
133	}
134	return result, nil
135}
136
137func createTablesIfDNE() {
138	_, err := DB.Exec(`CREATE TABLE IF NOT EXISTS user (
139  id INTEGER PRIMARY KEY NOT NULL,
140  username TEXT NOT NULL UNIQUE,
141  email TEXT NOT NULL UNIQUE,
142  password_hash TEXT NOT NULL,
143  active boolean NOT NULL DEFAULT false,
144  admin boolean NOT NULL DEFAULT false,
145  created_at INTEGER DEFAULT (strftime('%s', 'now'))
146);
147
148CREATE TABLE IF NOT EXISTS cookie_key (
149  value TEXT NOT NULL
150);`)
151	if err != nil {
152		log.Fatal(err)
153	}
154}
155
156// Generate a cryptographically secure key for the cookie store
157func generateCookieKeyIfDNE() []byte {
158	rows, err := DB.Query("SELECT value FROM cookie_key LIMIT 1")
159	defer rows.Close()
160	if err != nil {
161		log.Fatal(err)
162	}
163	if rows.Next() {
164		var cookie []byte
165		err := rows.Scan(&cookie)
166		if err != nil {
167			log.Fatal(err)
168		}
169		return cookie
170	} else {
171		k := make([]byte, 32)
172		_, err := io.ReadFull(rand.Reader, k)
173		if err != nil {
174			log.Fatal(err)
175		}
176		_, err = DB.Exec("insert into cookie_key values ($1)", k)
177		if err != nil {
178			log.Fatal(err)
179		}
180		return k
181	}
182}
183
184func main() {
185	configPath := flag.String("c", "flounder.toml", "path to config file") // doesnt work atm
186	flag.Parse()
187	args := flag.Args()
188	if len(args) < 1 {
189		fmt.Println("expected 'admin' or 'serve' subcommand")
190		os.Exit(1)
191	}
192
193	var err error
194	c, err = getConfig(*configPath)
195	if err != nil {
196		log.Fatal(err)
197	}
198	logFile, err := os.OpenFile(c.LogFile, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
199	if err != nil {
200		panic(err)
201	}
202	mw := io.MultiWriter(os.Stdout, logFile)
203	log.SetOutput(mw)
204
205	if c.HttpsEnabled {
206		_, err1 := os.Stat(c.TLSCertFile)
207		_, err2 := os.Stat(c.TLSKeyFile)
208		if os.IsNotExist(err1) || os.IsNotExist(err2) {
209			log.Fatal("Keyfile or certfile does not exist.")
210		}
211	}
212
213	// Generate session cookie key if does not exist
214	DB, err = sql.Open("sqlite3", c.DBFile)
215	if err != nil {
216		log.Fatal(err)
217	}
218
219	createTablesIfDNE()
220	cookie := generateCookieKeyIfDNE()
221	SessionStore = sessions.NewCookieStore(cookie)
222
223	switch args[0] {
224	case "serve":
225		wg := new(sync.WaitGroup)
226		wg.Add(2)
227		go func() {
228			runHTTPServer()
229			wg.Done()
230		}()
231		go func() {
232			runGeminiServer()
233			wg.Done()
234		}()
235		wg.Wait()
236	case "admin":
237		runAdminCommand()
238	}
239}