all repos — flounder @ cd4229413269e4ea5dc31bec7a4a2c5415c78fba

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