all repos — flounder @ 7c53875f77af9d5d12db25ab03042cfbbad58fb7

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